<!-- AI会话 -->

<template>
	<div style="height: 100%">
		<a-row :gutter="24" type="flex" align="stretch" style="height: 100%">
			<a-col :span="24" class="mb-24">
				<a-card :bordered="false" class="header-solid h-full" :bodyStyle="{ height: '100%', padding: '16px 10px' }">
					<a-col :span="5" style="height: 100%; padding: 0 6px">
						<div class="dialog_box">
							<div class="head" @click="createDialog('create', '')">点击新建会话</div>
							<div class="list">
								<div class="item" :class="{ active: item.id == dialogId }" v-for="(item, index) in dialogList" :key="'dialog_' + item.id" @click="dialogItem(item.id)">
									<div class="line1 c-line-1">{{ item.title }}</div>
									<div class="line2">
										<span>{{ item.createtime }}</span>
										<span>{{ item.msg_total }}条对话</span>
									</div>
									<div class="delete" @click.stop="deleteDialog(item.id)">
										<a-icon type="minus-circle" theme="filled" :style="{ fontSize: '24px', color: '#999', display: 'block' }" />
									</div>
									<div class="edit" @click.stop="createDialog('edit', item.id, item.title)">编辑</div>
								</div>
							</div>
							<div class="foot">
								<span class="c-pointer" @click="clearAllChat">清空所有会话</span>
							</div>
							<a-modal title="会话标题" :visible="createModal" cancelText="取消" okText="确定" :confirm-loading="createLoading" @ok="createOkHandle" @cancel="createModal = false">
								<a-input v-model="createTitle" placeholder="请输入会话标题" allow-clear />
							</a-modal>
						</div>
					</a-col>
					<a-col :span="19" style="height: 100%; padding: 0 6px">
						<div class="content_box">
							<div class="output" id="output">
								<div class="list_box" :class="{ active: dItem.id == dialogId }" v-if="dItem.id == dialogId" v-for="(dItem, dIndex) in dialogList" :key="'dlist_' + dItem.id">
									<div class="list" v-for="(item, index) in dItem.list" :key="'msg_' + item.id">
										<div class="item right" v-if="item.message">
											<div class="message">
												<v-md-editor v-model="item.message" mode="preview"></v-md-editor>
											</div>
											<div class="avatar">
												<img :src="userInfo.avatar" alt="" />
											</div>
										</div>
										<div class="item left">
											<div class="avatar">
												<img :src="config.logo ? config.logo : require('@/assets/imgs/logo.png')" alt="" />
											</div>
											<div class="message" v-if="item.response">
												<v-md-editor v-model="item.response" mode="preview"></v-md-editor>
												<div class="tools" v-if="!item.transmit">
													<div class="copy" @click="messageCopy(item.response)">
														<a-icon type="copy" />
														<span>复制</span>
													</div>
													<div class="collect" v-if="item.message && item.id && item.id != 'message'" @click="messageCollect(item)">
														<a-icon type="heart" :theme="item.vote == 0 ? 'outlined' : 'filled'" :style="{ color: item.vote == 0 ? '' : '#FF3434' }" />
														<span>收藏</span>
													</div>
												</div>
											</div>
											<div class="message" v-else>
												<a-icon type="loading" :style="{ fontSize: '26px' }" />
											</div>
										</div>
									</div>
								</div>
							</div>
							<div class="tools">
								<a-radio-group v-model="modeType" button-style="solid" size="small">
									<a-radio-button :value="item.type" v-for="(item, index) in modeList" :key="'g_mode_' + index">{{ item.name }}（{{ item.usable }}积分）</a-radio-button>
								</a-radio-group>
								<div class="clear c-pointer" @click="clearAloneChat">清空对话</div>
							</div>
							<div class="input">
								<textarea class="textarea" v-model="inputText" placeholder="请输入内容（Enter 发送消息 / Ctrl + Enter 换行）" @keyup.enter="handleKeyCode"></textarea>
								<div class="button">
									<a-button type="primary" size="small" :disabled="!disabled" @click="sendMessage"> 发送 </a-button>
								</div>
							</div>
						</div>
					</a-col>
				</a-card>
			</a-col>
		</a-row>
	</div>
</template>

<script>
	import { mapState, mapGetters, mapMutations, mapActions } from "vuex"
	export default {
		components: {},
		data() {
			return {
				inputText: "", // 发送数据
				disabled: false, // 按钮状态
				dialogList: [], // 会话列表
				dialogId: 0, // 当前会话id
				createModal: false, // 创建会话标题弹窗
				createLoading: false, // 异步加载状态
				createTitle: "新建会话", // 新建会话标题
				modalType: "create", // create / edit
				editId: "", // 编辑标题ID
				modeList: [], // 模型列表
				modeType: "" // 选中模型
			}
		},
		computed: {
			...mapGetters("user", ["token", "userInfo"]),
			...mapGetters("app", ["config"])
		},
		watch: {
			inputText(newValue, oldValue) {
				this.disabled = newValue == "" ? false : true
			}
		},
		created() {
			this.getDialogList()
			this.getModeList()
		},
		mounted() {},
		methods: {
			// 清除单个对话
			clearAloneChat() {
				const arr = this.dialogList.filter(item => item.transmit == true).map(item => item.id)
				if (arr.length) {
					this.$message.warning("请等待当前对话完成...")
					return
				}
				this.$confirm({
					title: "确定要清空当前对话吗？",
					content: "",
					okText: "清空",
					okType: "danger",
					cancelText: "取消",
					onOk: async () => {
						return await new Promise((resolve, reject) => {
							this.$http("chat.clearAlone", { type: "chat", group_id: this.dialogId }).then(res => {
								if (res.code === 1) {
									this.$message.success("清空成功")
									this.getChatHistory()
									resolve()
								} else {
									reject(res.msg)
								}
							})
						}).catch(error => {
							this.$message.error(error)
						})
					},
					onCancel() {}
				})
			},
			// 清空所有对话
			clearAllChat() {
				const arr = this.dialogList.filter(item => item.transmit == true).map(item => item.id)
				if (arr.length) {
					this.$message.warning("请等待当前对话完成...")
					return
				}

				this.$confirm({
					title: "确定要清空所有会话吗？",
					content: () => <div style="color: red;">清空后数据不可恢复!</div>,
					okText: "清空",
					okType: "danger",
					cancelText: "取消",
					onOk: async () => {
						return await new Promise((resolve, reject) => {
							this.$http("chat.clearAll", { type: "chat" }).then(res => {
								if (res.code === 1) {
									this.$message.success("清空成功")
									this.getDialogList()
									resolve()
								} else {
									reject(res.msg)
								}
							})
						}).catch(error => {
							this.$message.error(error)
						})
					},
					onCancel() {}
				})
			},
			// 获取GPT模型
			getModeList() {
				this.$http("chat.mode").then(res => {
					if (res.code === 1) {
						this.modeList = res.data
						this.modeType = res.data[0].type
					}
				})
			},
			// 信息复制
			async messageCopy(text) {
				try {
					await navigator.clipboard.writeText(text)
					this.$message.success("已复制到剪切板")
				} catch (err) {
					this.$message.error("复制失败")
				}
			},
			// 信息收藏
			messageCollect(item) {
				if (!item.id || item.id == "message") return
				this.$http("chat.collect", { type: "chat", msg_id: item.id }).then(res => {
					if (res.code === 1) {
						item.vote = item.vote == 0 ? 1 : 0
						this.$message.success(res.msg)
					}
				})
			},
			// 新建会话标题
			createOkHandle() {
				if (!this.createTitle) {
					this.$message.warning("请输入会话标题")
					return
				}
				const obj = { id: this.editId, title: this.createTitle }
				this.createLoading = true
				this.$http("chat.create", obj).then(res => {
					if (res.code === 1) {
						this.getDialogList()
						this.$message.success(res.msg)
					} else {
						this.$message.error(res.msg)
					}
					this.createModal = false
					this.createLoading = false
				})
			},
			// 创建/修改 会话
			createDialog(type, id, title) {
				const arr = this.dialogList.filter(item => item.transmit == true).map(item => item.id)
				if (arr.length) {
					this.$message.warning("请等待当前对话完成...")
					return
				}
				if (type === "create") {
					this.editId = ""
					this.createTitle = "新建会话"
				} else if (type === "edit") {
					this.editId = id
					this.createTitle = title
				} else {
					console.log("未知类型 => 已拦截")
					return
				}
				this.modalType = type
				this.createModal = true
			},
			// 删除会话
			deleteDialog(id) {
				this.$confirm({
					title: "确定要删除当前对话吗？",
					content: "",
					okText: "删除",
					okType: "danger",
					cancelText: "取消",
					onOk: async () => {
						return await new Promise((resolve, reject) => {
							this.$http("chat.delete", { id }).then(res => {
								if (res.code === 1) {
									this.getDialogList()
									resolve()
								} else {
									reject(res.msg)
								}
							})
						}).catch(error => {
							this.$message.error(error)
						})
					},
					onCancel() {}
				})
				return
			},
			// 切换会话项
			dialogItem(id) {
				this.dialogId = id
				this.dialogList.map(item => {
					if (item.id == id) {
						if (!item.transmit && !item.list.length) this.getChatHistory()
					}
				})
			},
			// 会话列表
			getDialogList() {
				this.$http("chat.list").then(res => {
					if (res.code === 1) {
						res.data.map((item, index) => {
							item.list = []
							item.transmit = false
						})
						this.dialogList = res.data.reverse()
						this.dialogId = this.dialogList[0].id
						this.getChatHistory()
					}
				})
			},
			// 监听按键 Ctrl + Enter 发送消息 / Enter 换行
			handleKeyCode(event) {
				if (event.keyCode == 13) {
					if (!event.ctrlKey) {
						event.preventDefault()
						this.inputText && this.sendMessage() // 发消息
					} else {
						this.inputText && (this.inputText = this.inputText + "\n") // 换行
					}
				}
			},
			// 历史记录
			getChatHistory() {
				this.$http("chat.history", { group_id: this.dialogId }).then(res => {
					if (res.code === 1) {
						this.dialogList.map(item => {
							if (item.id == this.dialogId) {
								item.list = res.data.message_list.reverse()
								item.msg_total = res.data.message_count
							}
						})
					}
				})
			},
			// 发送内容
			sendMessage() {
				this.dialogList.map(item => {
					if (item.id == this.dialogId) {
						if (item.transmit) {
							this.$message.warning("请等待当前对话完成...")
						} else {
							if (!this.inputText) {
								console.log("输入为空")
								return
							}
							if (!this.modeType) {
								this.$message.error("会话模型错误，请联系管理员")
								return
							}
							this.fetchDataStream(this.inputText, this.dialogId, this.modeType)
						}
					}
				})
			},
			// 发送请求
			async fetchDataStream(message, dId, mode) {
				if (!message) {
					console.log("输入为空")
					return
				}
				this.inputText = ""
				this.dialogList.map(item => {
					if (item.id == dId) {
						item.list.unshift({
							id: "message",
							message,
							response: "",
							transmit: true,
							vote: 0
						})
						item.transmit = true
						item.msg_total = Number(item.msg_total) + 1
					}
				})

				this.disabled = true
				const postData = { type: "chat", message, group_id: dId, mode },
					url = this.$BASE_API + "/addons/chatgpt/web/sendText",
					controller = new AbortController(),
					Token = this.token,
					Sign = window.location.search.replace(/\?/g, "")

				try {
					this.dialogList.map(item => {
						if (item.id == dId) {
							item.list[0].response = ""
						}
					})

					const response = await fetch(url, {
						method: "post",
						headers: {
							"Content-Type": "application/json;charset=utf-8",
							Token,
							Sign
						},
						body: JSON.stringify(postData),
						signal: controller.signal
					})

					const reader = response.body.getReader()
					let data = ""

					while (true) {
						const { done, value } = await reader.read(),
							str = new TextDecoder().decode(value)

						if (str.indexOf("data: [DONE]") != -1 || str.indexOf("data:[DONE]") != -1 || done) {
							const arr = str.replaceAll(" ", "").split("data:[DONE]")
							if (arr[0].length) {
								this.dialogList.map(item => {
									if (item.id == dId) {
										item.list[0].response += arr[0]
									}
								})
							}

							this.dialogList.map(item => {
								if (item.id == dId) {
									item.transmit = false
									item.list[0].transmit = null
									item.list[0].id = arr[1]
								}
							})
							break
						}

						data += str
						this.dialogList.map(item => {
							if (item.id == dId) {
								item.list[0].response = data
							}
						})
					}
				} catch {
					console.error("请求失败")
				}
			}
		}
	}
</script>

<style lang="scss" scoped>
	.dialog_box {
		height: 0;
		min-height: 100%;
		display: flex;
		flex-direction: column;
		border-radius: 12px;
		background: #f7f7f7;
		padding: 12px 0;

		.head {
			text-align: center;
			padding: 12px;
			background: #fff;
			border-radius: 10px;
			margin: 0 12px;
			cursor: pointer;
			color: #000;
			font-weight: bold;
			box-shadow: 1px 1px 10px 0 rgba(#000, 0.05);
		}

		.list {
			flex: 1;
			height: 100%;
			padding: 12px;
			overflow: hidden;
			margin-top: 12px;

			&:hover {
				overflow-y: scroll;
				overflow-x: hidden;
				padding-right: 0;
			}

			&::-webkit-scrollbar {
				width: 12px;
			}

			&::-webkit-scrollbar-thumb {
				border-radius: 12px;
				border: 4px solid rgba(0, 0, 0, 0);
				box-shadow: 4px 0 0 #a5adb7 inset;
			}

			&::-webkit-scrollbar-thumb:hover {
				box-shadow: 4px 0 0 #4a4a4a inset;
			}

			// 滚动条两端按钮
			&::-webkit-scrollbar-button {
				height: 10px;
			}

			.item {
				padding: 20px 12px;
				background: #fff;
				border-radius: 10px;
				margin-bottom: 12px;
				cursor: pointer;
				position: relative;

				&:last-child {
					margin-bottom: 0;
				}

				&.active {
					border: 1px solid #1890ff;
					box-shadow: 1px 1px 10px 0 rgba(#1890ff, 0.2);
				}

				&:hover {
					.delete {
						display: block;
					}
				}

				.edit {
					position: absolute;
					top: 4px;
					right: 10px;
					color: #1890ff;
					font-size: 12px;
					font-weight: 700;

					&:hover {
						text-shadow: 1px 1px 3px rgba(#000, 0.5);
					}
				}

				.line1 {
					color: #000;
					font-weight: 700;
				}

				.line2 {
					display: flex;
					align-items: center;
					justify-content: space-between;
					white-space: nowrap;
					flex-wrap: wrap;
					margin-top: 4px;
					color: #999;
				}

				.delete {
					position: absolute;
					top: 0;
					left: 0;
					transform: translate(-30%, -30%);
					background: #fff;
					border-radius: 50%;
					overflow: hidden;
					display: none;
				}
			}
		}

		.foot {
			margin-top: 10px;
			text-align: center;

			span {
				color: #1890ff;
				cursor: pointer;
				font-size: 14px;
			}
		}
	}
	.content_box {
		height: 0;
		min-height: 100%;
		display: flex;
		flex-direction: column;

		// 滚动条整体
		::-webkit-scrollbar {
			width: 6px;
			height: 6px;
		}
		// 滚动条滑块
		::-webkit-scrollbar-thumb {
			background-clip: padding-box;
			background-color: #a5adb7;
			border: 1px dashed rgba(0, 0, 0, 0);
			border-radius: 6px;
		}
		// 滚动条滑块 hover
		::-webkit-scrollbar-thumb:hover {
			background: #4a4a4a;
		}
		// 滚动条轨道
		::-webkit-scrollbar-track {
			background-color: transparent;
		}
		// 滚动条两端按钮
		::-webkit-scrollbar-button {
			height: 10px;
		}

		.output {
			flex: 1;
			border-radius: 12px;
			background: #f7f7f7;
			overflow: hidden;
			display: flex;
			flex-direction: column;

			.list_box {
				overflow-y: auto;
				overflow-x: hidden;
				display: flex;
				flex-direction: column-reverse;
				position: relative;
				z-index: 1000;
				padding: 20px;
				margin: 0 6px;

				.list {
					margin-bottom: 20px;

					.item {
						margin-bottom: 20px;
						display: flex;
						flex-wrap: nowrap;
						$imgsize: 40px;

						.avatar {
							width: $imgsize;
							height: $imgsize;
							border-radius: 50%;
							overflow: hidden;
							border: 2px solid #fff;

							img {
								width: 100%;
								height: 100%;
								object-fit: cover;
							}
						}

						.message {
							max-width: calc(100% - #{$imgsize + 20px} * 2);
							padding: 10px 12px;

							::v-deep .v-md-editor__right-area {
								width: auto;
								min-width: 0;
							}

							::v-deep .vuepress-markdown-body {
								padding: 0;
							}
						}

						&.left {
							justify-content: flex-start;

							.avatar {
								margin-right: 20px;
							}

							.message {
								border-radius: 0 12px 12px 12px;
								background: #fff;
								color: #000;
								position: relative;

								&:hover {
									.tools {
										display: flex;
									}
								}

								.tools {
									position: absolute;
									bottom: 0;
									left: 0;
									transform: translateY(100%);
									padding-top: 10px;
									display: flex;
									align-items: center;
									display: none;

									.copy,
									.collect {
										padding: 4px 10px;
										border: 1px solid #eee;
										border-radius: 6px;
										cursor: pointer;
										white-space: nowrap;

										&:hover {
											box-shadow: 1px 1px 10px 0 rgba(#1890ff, 0.2);
											background: #fff;
										}

										span {
											margin-left: 4px;
										}
									}

									.collect {
										margin-left: 12px;
									}
								}
							}
						}

						&.right {
							justify-content: flex-end;

							.avatar {
								margin-left: 20px;
							}

							.message {
								border-radius: 12px 0 12px 12px;
								background: $message_color;
								color: #fff;
								::v-deep .vuepress-markdown-body {
									background: $message_color;
									color: #fff;
								}
							}
						}
					}
				}
			}
		}
		.tools {
			padding: 10px 0;
			position: relative;

			.clear {
				position: absolute;
				top: 50%;
				right: 2px;
				transform: translateY(-50%);
				font-size: 14px;
				color: #1890ff;
			}
		}
		.input {
			height: 120px;
			position: relative;
			background: #f7f7f7;
			border-radius: 12px;
			padding: 12px 84px 12px 12px;

			.textarea {
				width: 100%;
				height: 100%;
				border: none;
				outline: none;
				resize: none;
				background: transparent;
				padding: 0;
				margin: 0;
			}

			.button {
				position: absolute;
				bottom: 12px;
				right: 12px;
			}
		}
	}
</style>
